package com.zhihao.aop;

import com.zhihao.types.annotations.DCCValue;
import com.zhihao.types.annotations.RateLimiterAccessInterceptor;
import jodd.util.StringUtil;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.Signature;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.redisson.api.*;
import org.springframework.stereotype.Component;

import javax.annotation.Resource;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.time.Duration;

/**
 * @author Jackson
 * @create 2025/5/29 9:03
 * @description 分布式限流
 */
@Slf4j
@Aspect
@Component
public class RateLimiterAOP {

    @Resource
    private RedissonClient redissonClient;

    // 限流开关，默认close-不限流
    @DCCValue("rateLimiterSwitch:close")
    private String rateLimiterSwitch;

    /* 切点 */
    @Pointcut("@annotation(com.zhihao.types.annotations.RateLimiterAccessInterceptor)")
    public void rateLimiterPoint() {}

    // @annotation直接将切点的注解作为参数传递，无需使用joinPoint再去获取方法上的注解及注解中的信息
    @Around("rateLimiterPoint() && @annotation(rateLimiterAccessInterceptor)")
    public Object rateLimitProcess(ProceedingJoinPoint joinPoint, RateLimiterAccessInterceptor rateLimiterAccessInterceptor) throws Throwable {
        // 0. 限流开关【open 开启、close 关闭】关闭后，不会走限流策略
        if (StringUtils.isBlank(rateLimiterSwitch) || "close".equals(rateLimiterSwitch)) {
            return joinPoint.proceed();
        }
        // 1. 获取注解的Key值（userId）
        String key = rateLimiterAccessInterceptor.key();
        if (StringUtil.isBlank(key)) {
            // 限流注解未正确配置值
            throw new RuntimeException("annotation RateLimiter userId is null！");
        }
        // 2.获取拦截字段key-userId的值value（eg:visitor）
        String keyAttr = getAttrValue(key, joinPoint.getArgs());
        log.info("aop key attribution is {}", keyAttr);
        if (StringUtil.isBlank(keyAttr)) {
            throw new RuntimeException("input args error,userId is null");
        }

        // 3.用户已被限流次数的redis缓存
        /*
            RAtomicLong：Redisson分布式原子长整型类
            1.延迟初始化机制
                Redisson 不会立即创建 Redis key，而是返回一个代表该 key 的 RAtomicLong 代理对象。
                实际创建时机：首次执行写操作（如 incrementAndGet(), set()）时自动初始化。
            2.读操作的特殊处理
                如果执行 get() 读操作且 key 不存在：
                ✅ 返回 0（不会创建 key）
            3.写操作的初始化
                首次执行写操作时，Redisson 会：
                1️⃣ 在 Redis 中创建 key
                2️⃣ 初始化值为 0（原子长整型的默认值）
                3️⃣ 执行操作
         */
        String userRateLimiterTimesBlack = "rate_limiter_times_user_black_" + keyAttr;
        RAtomicLong userBlackProxy = redissonClient.getAtomicLong(userRateLimiterTimesBlack);

        // 4.First：黑名单拦截，检查用户是否已存在于'限流黑名单列表'中，即：该用户是否已被限流多次而进入24h限流黑名单中，直接拦截
        /* all:代表对所有用户限流，即接口限流  如果是接口限流规则不是接口限流，并且已配置了限流黑名单阈值（为0默认不额外添加限流黑名单即每次还需要进行限流器RRateLimiter检查）*/
        if (!"all".equals(keyAttr) && rateLimiterAccessInterceptor.blacklistCount() != 0) {
            /* 如果用户已有限流黑名单记录，且历史（24h内）被限流次数大于阈值，则触发黑名单拦截 */
            if (userBlackProxy.isExists() && userBlackProxy.get() > rateLimiterAccessInterceptor.blacklistCount()) {
                // 返回已配置的方法
                return fallbackMethodResult(joinPoint, rateLimiterAccessInterceptor.fallbackMethod());
            }else if (!userBlackProxy.isExists()){
                // 黑名单用户(此处可能没做到黑名单用户固定限制24h，早参与的违规后会被早解禁)
                userBlackProxy.expire(Duration.ofHours(24)); // 设置24小时过期
            }
        }

        // 5.用户限流器初始化(当用户首次执行接口的时候,需要创建限流器)
        boolean isNew = false;  // 首次/续期
        long ttl = rateLimiterAccessInterceptor.rateLimiterTTL();   // 限流器自动清理时间
        RRateLimiter rateLimiter = redissonClient.getRateLimiter(keyAttr);  // 令牌桶算法
        if (!rateLimiter.isExists()) {
            rateLimiter.trySetRate(
                    RateType.OVERALL,                     // 全局模式(针对所有分布式节点)
                    rateLimiterAccessInterceptor.permitsPerSecond(), // 每秒允许的接口请求数    时间窗口：固定为1秒（间隔1秒，单位秒）
                    1,                                    // 时间间隔
                    RateIntervalUnit.SECONDS              // 时间单位
            );
            isNew = true;
        }
        // 关键优化：设置/更新过期时间
        if (isNew) {
            // 新限流器：设置初始TTL
            rateLimiter.expire(Duration.ofMinutes(ttl));
        } else {
            // 已有限流器：每次访问续期
            rateLimiter.expire(Duration.ofMinutes(ttl));
        }

        // 6.限流执行逻辑 : 用户真正进行限流校验，尝试在令牌桶中获取令牌(容量为permitsPerSecond)
        if (!rateLimiter.tryAcquire()) { // 令牌获取失败，即超出每秒请求数，则用户被限流黑名单记录 +1
            if (rateLimiterAccessInterceptor.blacklistCount() != 0) {
                userBlackProxy.incrementAndGet(); // 增加黑名单计数
            }
            return fallbackMethodResult(joinPoint, rateLimiterAccessInterceptor.fallbackMethod()); // 执行限流返回
        }

        // 7. 正常请求，继续执行接口，并返回结果
        return joinPoint.proceed();
    }

    /**
     * 调用用户配置的回调方法，当拦截后，返回回调结果。
     */
    private Object fallbackMethodResult(JoinPoint jp, String fallbackMethod) throws NoSuchMethodException, InvocationTargetException, IllegalAccessException {
        Signature sig = jp.getSignature();
        MethodSignature methodSignature = (MethodSignature) sig;
        Method method = jp.getTarget().getClass().getMethod(fallbackMethod, methodSignature.getParameterTypes());
        return method.invoke(jp.getThis(), jp.getArgs());
    }


    /**
     * 实际根据自身业务调整，主要是为了获取通过某个值做拦截
     */
    public String getAttrValue(String attr, Object[] args) {
        // 由于draw接口的入参为String userId和 Long activityId，所以只要是String就是userId
        if (args[0] instanceof String) {
            return args[0].toString();
        }
        String filedValue = null;
        for (Object arg : args) {
            try {
                if (StringUtils.isNotBlank(filedValue)) {
                    break;
                }
                // filedValue = BeanUtils.getProperty(arg, attr);
                // fix: 使用lombok时，uId这种字段的get方法与idea生成的get方法不同，会导致获取不到属性值，改成反射获取解决
                filedValue = String.valueOf(this.getValueByName(arg, attr));
            } catch (Exception e) {
                log.error("获取路由属性值失败 attr：{}", attr, e);
            }
        }
        return filedValue;
    }

    /**
     * 获取对象的特定属性值
     *
     * @param item 对象
     * @param name 属性名
     * @return 属性值
     * @author tang
     */
    private Object getValueByName(Object item, String name) {
        try {
            Field field = getFieldByName(item, name);
            if (field == null) {
                return null;
            }
            field.setAccessible(true);
            Object o = field.get(item);
            field.setAccessible(false);
            return o;
        } catch (IllegalAccessException e) {
            return null;
        }
    }

    /**
     * 根据名称获取方法，该方法同时兼顾继承类获取父类的属性
     *
     * @param item 对象
     * @param name 属性名
     * @return 该属性对应方法
     * @author tang
     */
    private Field getFieldByName(Object item, String name) {
        try {
            Field field;
            try {
                field = item.getClass().getDeclaredField(name);
            } catch (NoSuchFieldException e) {
                field = item.getClass().getSuperclass().getDeclaredField(name);
            }
            return field;
        } catch (NoSuchFieldException e) {
            return null;
        }
    }
}
